Log 是系統運作時產生的記錄,可以是系統發生的事件、錯誤訊息或是其他 有助於釐清系統行為與問題的重要線索。一個最簡單、基礎的 Log 可以是由 Node.js 的 Console API 所產生,如下:
console.log('Hello World!');
console.error('Error!');
console.warn('Warning!');
在微服務的架構下 Log 是相當重要的,當服務發生問題時,能夠有效找出問題的方式就是觀察與分析 Log,想像現在有數十、數百個服務,如果沒有有效的 Log 幫助開發者釐清問題,就像是矇著雙眼玩解謎遊戲,解題難度大幅提升,更可能因此造成企業承受損失,導致一場災難的發生。
產生 Log 的方式有很多種,無論採用哪種方式最重要的是 留下易於解析、有效的資訊,在服務數量非常多且需要聚合、分析它們 Log 的情況下,規範 Log 產生的樣式、格式就變得十分重要。
一般來說,會依照 Log 的重要程度與使用場景進行等級分類,目前較常見的有六種:
目前主流的 Log 框架都有提供上述六種等級的 Log 產生方法, Node.js 生態圈中最常見的框架就是 winston 與 pino。
在預設情況下啟動 NestJS 應用程式,會在終端機印出一連串五顏六色的 Log 資訊,這些 Log 是透過 NestJS 內建的 ConsoleLogger
所產生,它是基於文字的 Log 產生器,透過調整排版與色彩來提升可讀性。
NestJS 在 Log 上提供了十足的彈性,不僅可以調整要顯示的 Log Level,甚至還可以讓開發者自行設計 Logger,用 NestJS 的開發風格來整合最符合團隊的 Log 規範。接下來就帶大家來了解一下 NestJS 的 Logger 設計吧!
在 NestJS 的架構下,Logger 必須實作 LoggerService
介面,進而規範不同 Log Level 的實作,下方是介面定義的方法名稱:
verbose
:對應到 TRACE 等級,不是必要的實作。debug
:對應到 DEBUG 等級,是必要的實作。log
:對應到 INFO 等級,是必要的實作。warn
:對應到 WARN 等級,是必要的實作。error
:對應到 ERROR 等級,是必要的實作。fatal
:對應到 FATAL 等級,不是必要的實作。不論是內建、自訂或第三方的 Logger 都會依照該介面進行實作,於是 NestJS 實作了一個叫 Logger
的 Helper 來 代表在 NestJS 應用程式套用的 Logger,如此一來,在程式碼中就可以統一使用 Logger
來產生 Log,減少因更換 Logger 而異動的部分。下方是範例程式碼,在 main.ts
使用 Logger
提供的上述六種方法來印出 Log:
import { NestFactory } from '@nestjs/core';
import { Logger } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
Logger.verbose('TRACE!');
Logger.debug('DEBUG!');
Logger.log('INFO!');
Logger.warn('WARN!');
Logger.error('ERROR!');
Logger.fatal('FATAL!');
}
bootstrap();
啟動應用程式後,會在終端機看到六種顏色的 Log:
如果希望只顯示某些 Level 的 Log,可以在 NestFactory
的 create
的選項參數中帶入 logger
參數,並以陣列方式指定要顯示的 Log Level。下方是範例程式碼,在 logger
指定顯示 INFO、WARN、ERROR 與 FATAL 四種等級 的 Log:
import { NestFactory } from '@nestjs/core';
import { Logger } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: ['log', 'warn', 'error', 'fatal'],
});
await app.listen(3000);
Logger.verbose('TRACE!');
Logger.debug('DEBUG!');
Logger.log('INFO!');
Logger.warn('WARN!');
Logger.error('ERROR!');
Logger.fatal('FATAL!');
}
bootstrap();
此時終端機就只會顯示符合這四種等級的 Log:
如果希望不顯示 Log,可以指派 logger
的值為 false
。下方是範例程式碼:
import { NestFactory } from '@nestjs/core';
import { Logger } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: false,
});
await app.listen(3000);
Logger.verbose('TRACE!');
Logger.debug('DEBUG!');
Logger.log('INFO!');
Logger.warn('WARN!');
Logger.error('ERROR!');
Logger.fatal('FATAL!');
}
bootstrap();
此時終端機將不會印出任何 Log:
如果不想使用 ConsoleLogger
的樣式,可以使用前面提到的 LoggerService
介面實作屬於自己的 Logger。下方是範例程式碼,設計了 MyLoggerService
並實作 LoggerService
介面:
import { LoggerService } from '@nestjs/common';
export class MyLoggerService implements LoggerService {
verbose(message: any, ...optionalParams: any[]) {
console.trace('TRACE', message, ...optionalParams);
}
debug(message: any, ...optionalParams: any[]) {
console.debug('DEBUG', message, ...optionalParams);
}
log(message: any, ...optionalParams: any[]) {
console.log('LOG', message, ...optionalParams);
}
warn(message: any, ...optionalParams: any[]) {
console.warn('WARN', message, ...optionalParams);
}
error(message: any, ...optionalParams: any[]) {
console.error('ERROR', message, ...optionalParams);
}
fatal(message: any, ...optionalParams: any[]) {
console.error('FATAL', message, ...optionalParams);
}
}
實作完畢後,可以將實例化的 MyLoggerService
指派給 logger
進而取代 ConsoleLogger
:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { MyLoggerService } from './logger';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: new MyLoggerService(),
});
await app.listen(3000);
}
bootstrap();
啟動應用程式後,會在終端機看到 MyLoggerService
定義的 Log 格式:
上方 Custom Logger 範例產生出來的 MyLoggerService
實例 沒有 被 NestJS 的 IoC Container 管理,如果沒有拆分實例的必要,這樣的作法不僅需要消耗較多資源,也會變得較難執行單元測試,於是 NestJS 有為此想了一個方式來讓 Custom Logger 也能由 NestJS 管理實例,首先,在 MyLoggerService
加上 @Injectable
裝飾器:
import { LoggerService, Injectable } from '@nestjs/common';
@Injectable()
export class MyLoggerService implements LoggerService {
// ...
}
接著,建立一個 LoggerModule
並將 MyLoggerService
放入 providers
中,同時將其匯出:
import { Module } from '@nestjs/common';
import { MyLoggerService } from './logger.service';
@Module({
providers: [MyLoggerService],
exports: [MyLoggerService],
})
export class LoggerModule {}
補充:此處的內容涉及 Provider 相關知識,針對 NestJS Provider 可以參考官方文件或是我之前分享的文章。
在 AppModule
使用 LoggerModule
以便在應用程式啟動時建立 MyLoggerService
實例:
import { Module } from '@nestjs/common';
import { LoggerModule } from './logger';
// ...
@Module({
imports: [LoggerModule],
// ...
})
export class AppModule {}
最後,調整 main.ts
的內容,透過 app
的 get
方法取出 MyLoggerService
實例,並透過 useLogger
方法進行套用,這裡要特別注意,建議在 NestFactory
的 create
選項參數中配置 bufferLogs
為 true
,因為建立 NestApplication
實例本身不參與依賴注入的初始化階段,所以在這時候所產生的 Log 依然會使用 ConsoleLogger
輸出,但只要將 bufferLogs
設為 true
就可以先將這些 Log 進行緩衝,直到透過 useLogger
套用的 Logger 生效才會進行輸出,確保格式統一:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { MyLoggerService } from './logger';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
bufferLogs: true,
});
app.useLogger(app.get(MyLoggerService));
await app.listen(3000);
}
bootstrap();
啟動應用程式後,會在終端機看到 MyLoggerService
定義的 Log 格式:
如果只想要調整 ConsoleLogger
部分方法,可以採取擴充的方式來實作,就不需要使用 Custom Logger 的方式重新造輪子。下方是範例程式碼,建立一個 MyConsoleLogger
的類別並繼承 ConsoleLogger
,這裡針對 log
的部分進行調整,設計 log
方法並將 message
參數放入物件中由父類別的 log
產生 Log:
import { ConsoleLogger, Injectable } from '@nestjs/common';
@Injectable()
export class MyConsoleLogger extends ConsoleLogger {
log(message: any, ...optionalParams: [...any, string?]): void {
super.log({ message }, ...optionalParams);
}
}
調整 main.ts
的內容,指派 logger
為 MyConsoleLogger
的實例即可套用:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { MyConsoleLogger } from './logger';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
logger: new MyConsoleLogger(),
});
await app.listen(3000);
}
bootstrap();
當然也可以採用 DI-based 的方式來管理實例:
import { NestFactory } from '@nestjs/core';
import { Logger } from '@nestjs/common';
import { AppModule } from './app.module';
import { MyConsoleLogger } from './logger';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
bufferLogs: true,
});
app.useLogger(app.get(MyConsoleLogger));
await app.listen(3000);
}
bootstrap();
注意:要記得將
MyConsoleLogger
掛在 Module 底下。
啟動應用程式後,會在終端機看到包裝成物件後的訊息:
雖然 NestJS 內建的 ConsoleLogger
可以產出容易閱讀的 Log,但在微服務架構下,各個服務如果產生的 Log 都是純文字,就會變得難以解析,所以比較好的 Log 格式可以 JSON 的方式呈現,這樣要做解析會變得容易許多。
前面有提到 Node.js 生態圈中最常見的 Log 框架是 winston 與 pino,這兩個框架都可以產生 JSON 格式的 Log,那該選哪個框架呢?以我個人來說會選擇 pino,原因是 pino 的效能表現較佳。
NestJS 社群有針對 pino 進行包裝,提供 nestjs-pino 套件讓 NestJS 開發者使用。透過下方指令安裝相關套件:
$ npm install nestjs-pino pino-http
補充:
pino-http
是基於 pino 的高效 HTTP Logger,可以針對每一個 HTTP Request 進行相關處理,讓 Log 變得更具有意義,有興趣的朋友可以參考官方文件。
在 AppModule
使用 nestjs-pino
提供的 LoggerModule
,並透過其 forRoot
或 forRootAsync
方法產生相關實例:
import { Module } from '@nestjs/common';
import { LoggerModule } from 'nestjs-pino';
// ...
@Module({
imports: [ LoggerModule.forRoot()],
// ...
})
export class AppModule {}
接著,調整 main.ts
的內容,透過 DI-based 的方式指定使用 nestjs-pino
的 Logger
:
import { NestFactory } from '@nestjs/core';
import { Logger } from 'nestjs-pino';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
bufferLogs: true,
});
app.useLogger(app.get(Logger));
await app.listen(3000);
}
bootstrap();
啟動應用程式後,會在終端機看到 JSON 格式的 Log:
雖然 Log 有正確轉換成 JSON 的格式,但在開發時還是以文字的方式呈現 Log 資訊會更容易閱讀,這時候就可以根據不同環境進行調整,pino 有推出 pino-pretty 來將 Log 轉換成文字格式。透過下方指令進行安裝:
$ npm install pino-pretty
安裝完之後,針對 LoggerModule
的部分進行調整,在 forRoot
帶入相關參數,僅在 NODE_ENV
不是 production
的時候才將 transport
設定為 pino-pretty
,並且將 level
設為 debug
:
import { Module } from '@nestjs/common';
import { LoggerModule } from 'nestjs-pino';
// ...
const isProduction = () => process.env.NODE_ENV === 'production';
@Module({
imports: [
LoggerModule.forRoot({
pinoHttp: {
level: !isProduction() ? 'debug' : 'info',
transport: !isProduction() ? { target: 'pino-pretty' } : undefined,
},
}),
],
// ...
})
export class AppModule {}
啟動應用程式後,會在終端機看到格式化後的 Log:
如果在啟動時設定環境變數 NODE_ENV
為 production
,終端機顯示的就會是 JSON 格式:
回顧一下今天的重點內容,在微服務架構下 Log 是相當重要的,透過有效的 Log 可以幫助我們釐清問題,若將 Log 依照重要程度與種類進行分類,在 Log 的分析、聚合上會更有幫助。NestJS 有內建 ConsoleLogger
來產生文字格式的 Log,甚至有定義出 loggerService
的介面,讓 Custom Logger、ConsoleLogger
使用相同介面進行設計,大幅提升一致性。
雖然 ConsoleLogger
產生的訊息在開發模式下容易閱讀,但在服務很多的情況下,還是以 JSON 格式進行 Log 分析、聚合會更容易,所以可以使用 NestJS 社群提供的 nestjs-pino
來協助產生。
在了解 NestJS 如何產生 Log 之後,下一篇將會介紹在微服務下要如何收集、解析 Log,敬請期待!